# Copyright (C) 2005-2006 Frederic Back <fredericback@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.

import random
import string
import threading
import time
import math

import gtk
from _guitools import *

try: import psycopg
except: 
    try: import psycopg2.psycopg1 as psycopg
    except:
        errorMessage(_("Library not found"), "Please install the psycopg library.")

import gobject

from _dbtools import *
from _options import *
from _symbol import SymbolRenderer

def joinBoxes(box_a, box_b):
    ''' Union two boxes. Useful to join geometry extents '''
    a_ll = box_a.getLowerLeft()
    b_ll = box_b.getLowerLeft()
    a_ur = box_a.getUpperRight()
    b_ur = box_b.getUpperRight()
    if a_ll.getX() < b_ll.getX(): mx = a_ll.getX()
    else: mx = b_ll.getX()
    if a_ll.getY() < b_ll.getY(): my = a_ll.getY()
    else: my = b_ll.getY()
    if a_ur.getX() > b_ur.getX(): Mx = a_ur.getX()
    else: Mx = b_ur.getX()
    if a_ur.getY() > b_ur.getY(): My = a_ur.getY()
    else: My = b_ur.getY()
    return Box( Point(mx,my), Point(Mx,My) )

#---------------------------------------------------------------------------------------------------
class Colour:
    def __init__(self,r=0,g=0,b=0,a=65535):
        self.setFromRGBA(r,g,b,a)

    def setFromRGBA(self,r,g,b,a=65535):
        self.r = int(r)
        self.g = int(g)
        self.b = int(b)
        self.a = int(a)

    def asCairoRGBA(self):
        return [float(self.r)/65535.0,float(self.g)/65535.0,float(self.b)/65535.0,float(self.a)/65535.0]

    def asCairoRGB(self):
        return [float(self.r)/65535.0,float(self.g)/65535.0,float(self.b)/65535.0]

    def asGdkColor(self):
        return gtk.gdk.Color(int(self.r),int(self.g),int(self.b))

    def __str__(self):
        return str(self.r)+","+str(self.g)+","+str(self.b)+","+str(self.a)

    def fromString(self, string):
        try: self.setFromRGBA(*str(string).split(","))
        except: raise ValueError, _("Could not parse colour from %s")%string
        
    def copy(self):
        return Colour(self.r,self.g,self.b,self.a)

def colour_from_gdk(gdkcolor, alpha = 65535):
    return Colour(gdkcolor.red,gdkcolor.green,gdkcolor.blue,alpha)
#---------------------------------------------------------------------------------------------------
class Project( gobject.GObject ):
    """ A collection if ResultSets. Basically, the toplevel object instance.
        There can be only one. """

    __gsignals__ = {
    
        # called when a layer's query changes. Toplevel because of managment overhead.
        'query-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        
        'resultset-added': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
    }

    def __init__(self):
        gobject.GObject.__init__(self)

        self.resultsets = []
        self.filename = None

    def addResultset(self, resultset):
        if resultset.project is not None:
            resultset.project.remResultset(resultset)

        self.resultsets.append(resultset)
        resultset.project = self
        self.emit("resultset-added",resultset)

    def insertResultset(self, pos, resultset):
        if resultset.project is not None:
            resultset.project.remResultset(resultset)

        self.resultsets.insert(pos,resultset)
        resultset.project = self
        self.emit("resultset-added",resultset)

    def remResultset(self, resultset):
        self.resultsets.remove(resultset)
        resultset.project = None
        resultset.emit("removed")

#---------------------------------------------------------------------------------------------------
class ResultSet( gobject.GObject ):
    """ A collection of layers. Together, they form a map. """

    count = 1 # used for incremental naming

    __gsignals__ = {
        'layer-added': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'layer-removed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'removed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'values-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    def __init__(self):
        gobject.GObject.__init__(self)
        self.project = None
        self.viewExtent = None
        self.name = "Map %i" % ResultSet.count
        ResultSet.count += 1
        self.layers = []

    def setViewArea(self, extent):
        """ Set the view area on the map. 
            If it is None, the program will compute it on the fly. """
        self.viewExtent = extent

    def addLayer(self, layer):
        if layer.resultset is not None:
            layer.resultset.remLayer(layer)

        self.layers.append(layer)
        layer.resultset = self
        self.emit("layer-added",layer)

    def insertLayer(self, pos, layer):
        if layer.resultset is not None:
            layer.resultset.remLayer(layer)

        self.layers.insert(pos,layer)
        layer.resultset = self
        self.emit("layer-added",layer)

    def remLayer(self, layer):
        self.layers.remove(layer)
        layer.resultset = None
        self.emit("layer-removed",layer)

    def copy(self):
        """ Return a shallow copy of the layer, without the data """
        result = ResultSet()
        result.name = self.name
        result.viewExtent = self.viewExtent
        for layer in self.layers:
            result.addLayer(layer.copy())
        return result

#-------------------------------------------------------------------------------
class Layer( gobject.GObject ):
    """ A layer represents query result """

    # static attributes
    count = 1

    # gtk signals
    __gsignals__ = {
        'status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)),
        'values-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'style-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'fetcher-left-thread': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'extent-computed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),

        "source-reset": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    def __init__(self):
        gobject.GObject.__init__(self)
        
        self.resultset = None
        self.name = "Layer %i" % Layer.count

        Layer.count += 1
        self.loaded = False
        self.__resetAttributes()
        self.style = LayerStyle()
        self.geometryType = None
        self.lock = threading.Lock()

        # Layer instances can aggregate a data fetcher instance to feed them
        # with data. When constructed, a layer does not yet have one.
        self.status = _('not loaded')
        self.__fetcher = None

    def __del__(self):
        if self.__fetcher:
            self.__fetcher.quit = True

    # currently simply returns data at x,y
    def getGeometry(self, index, column):
        if index >= len(self.data): return None
        self.lock.acquire()
        row = self.data[index]
        self.lock.release()
        if column >= len(row): return None
        return row[column]
        
    def super_experimental_drawing(self,plotter):
        for row in self.data:
            for col in self.geomCols:
                g = row[col]
                g.draw(g,plotter)

    def get_data(self, index, column):
        """ Return data at a certain position. """
        if index >= len(self.data): return None
        self.lock.acquire()
        row = self.data[index]
        self.lock.release()
        if column >= len(row): return None
        return row[column]

    def extractExtentFromQuery(self, extent):
        """ get the extent of the layer by peeking at the query. """
        pass 

    # get data from a cursor
    def setDataSource(self, cursor, query, tables):
        """ Initiate the data fetching process, but do not launch it.
        cursor: A db api compliant Cursor instance.
        query: A string containing the sql query.
        tables: A list of Table objects which are getting queried. """

        # initialize
        self.__resetAttributes()
        self.query = query
        self.size = 0
        self.totalsize = cursor.rowcount
        self.query = query
        self.queryDescription = cursor.description

        # ------------------------------------------------------------- geomcols
        # Scan the cursor description for columns containing geometry.
        # Compares values with the registered psycopg types.
        for i in range( len(cursor.description) ):
            typeID = cursor.description[i][1]
            if typeID not in psycopg.types: continue
            if psycopg.types[typeID].name == "Geometry": self.geomCols.append(i)

        # --------------------------------------------------- create the fetcher
        self.__fetcher = DataFetcher(self,cursor)

        # -------------------------------------------------- compute view extent
        # Since we want to draw a map as soon as the Layer gets filled with data,
        # we need to now an extent of the map to zoom on beforehand. To achieve
        # this, we first look if the tables passed in (see arguments) contain
        # an extent that can be joined...
        extent = None
        for table in tables:
            if extent is None: extent = table.extent
            elif table.extent is not None: extent = joinBoxes(extent,table.extent)
        self.extent = extent

        # ... if we were not successfull, it means that no tables were used in the
        # query (for instance by "SELECT MakePoint(x,y)), or that the tables'
        # extent is not available. In this case, we tell the data fetcher to 
        # *guess* the total extent (see heuristics explained there). 
        if self.extent == None: self.__fetcher.approximateExtent = True

        self.emit("source-reset")

    def stopLoading(self):
        if self.__fetcher: self.__fetcher.quit = True

    def isLoading(self):
        if self.__fetcher:
            if self.__fetcher.isAlive() and self.__fetcher.quit is False:
                return True
        return False

    def loadData(self):
        if self.isLoading(): raise Exception, _('Trying to load data, but is already loading')
        if self.__fetcher: self.__fetcher.start()
        else: raise Exception, _('Layer cannot reload data for security reasons.')

    def setStyle(self, style):
        self.style = style
        self.emit("style-changed")

    def copy(self):
        """ Return a shallow copy of the layer, without the data """
        layer = Layer()
        layer.name = self.name
        layer.query = self.query
        layer.queryDescription = self.queryDescription # important!
        layer.style = self.style.copy()
        layer.extent = self.extent
        return layer

    def __resetAttributes(self):
        self.query = None
        self.queryDescription = None    # the according cursor.descriptionqueryDescription
        self.size = 0
        self.totalsize = 0
        self.geomCols = [ ]             # stores columns that contain geometry
        self.data = [ ]                 # rows of data
        self.extent = None

#---------------------------------------------------------------------------------------------------
class LayerStyle( gobject.GObject ):
    """ A collection of rendering properties. 
    
    
        ---labels---
        A layer may contain several geometry column, but ONLY ONE layer column.
        
        
    """

    defaultFillPalette = None
    defaultLinePalette = None
    fillPaletteIndex = 0
    linePaletteIndex = 0

    def __init__(self):
        gobject.GObject.__init__(self)
        self.resultset = None

        # fill and outline:
        if LayerStyle.defaultFillPalette is not None:
            if LayerStyle.fillPaletteIndex >= len(LayerStyle.defaultFillPalette):
                LayerStyle.fillPaletteIndex = 0
            self.fill = Colour(*LayerStyle.defaultFillPalette[LayerStyle.fillPaletteIndex])
            LayerStyle.fillPaletteIndex += 1
        else: self.fill = Colour(45000,30000,50000)
        self.outline = Colour(0,0,0)
    
        # line and linewidth
        if LayerStyle.defaultLinePalette is not None:
            if LayerStyle.linePaletteIndex >= len(LayerStyle.defaultLinePalette):
                LayerStyle.linePaletteIndex = 0
            self.line = Colour(*LayerStyle.defaultLinePalette[LayerStyle.linePaletteIndex])
            LayerStyle.linePaletteIndex += 1
        else: self.line = Colour(45000,30000,50000)
        self.linewidth = 1.0

        # symbol and symbol scaling
        self.symbol = SymbolRenderer.commonSymbols["circle 3"]
        self.symbolsize = 1.0
        
        # a list of column indices to use as labels
        self.labelFlag = False
        self.labelcolumn = -1
        self.labelsyntax = "%s"
        self.labelfont = "sans 12"
        self.labelcolour = Colour(0,0,0)
        
    def copy(self):
        """ Create a copy of self. """
        style = LayerStyle()
        style.fill = self.fill.copy()
        style.outline = self.outline.copy()
        style.line = self.line.copy()
        style.linewidth = self.linewidth
        style.symbol = self.symbol
        style.symbolsize = self.symbolsize
        style.labelFlag = self.labelFlag
        style.labelcolumn = self.labelcolumn
        style.labelsyntax = self.labelsyntax
        style.labelfont = self.labelfont
        style.labelcolour = self.labelcolour.copy()
        return style

#---------------------------------------------------------------------------------------------------
class DataFetcher(threading.Thread):
    ''' Fetch data from a database cursor object.'''

    def __init__(self, layer, cursor):
        threading.Thread.__init__(self)

        self.cursor = cursor
        self.limit = True
        self.layer = layer

        self.screenTableRows = 30 # assume that 30 table rows are visible on screen
        self.maxListstoreRows = 0 # is set to 2000 below
        self.exectime = 0       # once done, stores total execution time

        # used to compute the layers' actual extents. Off by default.
        self.approximateExtent = False
        self.mx, self.my = None, None
        self.Mx, self.My = None, None

        # a simple logic to display fetching progress every few ticks only
        chunkCount = 20
        maxChunk = 50
        self.chunksize = int(layer.totalsize/chunkCount)
        if self.chunksize > maxChunk: self.chunksize = maxChunk # chunksize always <= 50!
        self.chunk = self.chunksize # first chunk


    def run(self):
        self.quit = False
        starttime = time.clock()
        row = self.cursor.fetchone()

        gtk.gdk.threads_enter()
        self.layer.status = "0%%"
        self.layer.emit("status-changed",self.layer.status)
        gtk.gdk.threads_leave()

        if self.approximateExtent and Options.verbose:
            print "Extent of layer '%s' unknown. Compute it!" %self.layer.name
        
        while self.quit is False and row is not None:

            # See Layer.setDataSource() for information about why this is nessesary.    
            # Try to get the layer's extent by extracting one point from each geometry.
            # This point then extents the current extent.
            if self.approximateExtent:
                if len(row) > 0:
                    for c in self.layer.geomCols:
                        self.__addGeometryToExtents(row[c])
    
            self.layer.lock.acquire()
            self.layer.size += 1
            self.layer.data.append(row)
            self.layer.lock.release()

            if self.layer.size == 1: self.__computeGeometryType(row)
            
            if self.quit is False: row = self.cursor.fetchone()
            
            if self.layer.size > self.chunk:
                self.chunk += self.chunksize
                f = float(self.layer.size)/self.layer.totalsize
                if f <= 1.0: # range always 0 .. 1
                    gtk.gdk.threads_enter()
                    self.layer.status = "%i%%"%(f*100)
                    self.layer.emit("status-changed", self.layer.status)
                    gtk.gdk.threads_leave()

        # emit the layer's total extent after loading
        if self.approximateExtent:
            b = Box( Point(self.mx,self.my), Point(self.Mx,self.My) )
            self.layer.extent = b
            print "extent computed:",b
            gtk.gdk.threads_enter()
            self.layer.emit("extent-computed")
            gtk.gdk.threads_leave()

        self.exectime = time.clock()-starttime
        print "Time to fetch %i rows: %s" %(self.layer.size,str(self.exectime))

        self.cursor.close()
        
        if self.quit is True:
            self.layer.totalsize = self.layer.size
            gtk.gdk.threads_enter()
            self.layer.status = "stopped"
            self.layer.emit("status-changed",self.layer.status)
            gtk.gdk.threads_leave()
        else:
            self.layer.loaded = True
            gtk.gdk.threads_enter()
            self.layer.status = None
            self.layer.emit("status-changed",self.layer.status)
            gtk.gdk.threads_leave()

        #print "time:",str(time.time()-t),"sec"

        gtk.gdk.threads_enter()
        self.layer.emit("fetcher-left-thread")
        gtk.gdk.threads_leave()

    #--------------------------------------------------------------------------

    def __computeGeometryType(self, row):
        """ Transmitted after the datafetcher got the first row of data. """
        
        # scan the row for geometry type
        t = []
        for col in self.layer.geomCols:
            g = row[col]
            n = g.__class__.__name__
            if n == "OGPoint": t.append("point")
            elif n == "OGLineString": t.append("line")
            elif n == "OGPolygon": t.append("polygon")
            elif n == "OGGeometryCollection": t.append("multi")
            else: # handle "OGMulti" etc
                #print n,"found, get subgeometries." 
                for subg in g.getGeometries():
                    n = subg.__class__.__name__
                    if n == "OGPoint": t.append("point")
                    elif n == "OGLineString": t.append("line")
                    elif n == "OGPolygon": t.append("polygon")
                    else:
                        raise Exception, "Unknown geometry type %s"%n

        # set to None if no type found
        if len(t) == 0: geomtype = None
        else: geomtype = t[0]

        # set to "multi" if more than one type found
        for e in t:
            if e != geomtype: geomtype = "multi"

        self.layer.geometryType = geomtype
        #print "Geometry type of layer %s: %s"%(self.layer.name,geomtype)

        gtk.gdk.threads_enter()   
        self.layer.emit("values-changed")
        gtk.gdk.threads_leave()

    def __addGeometryToExtents(self, g):
        """ This method is used to find out the map extent of the geometry.
            
            It contains some realllllly dirty hacks, which are here to stay
            until I port this stuff to c and hence can use geos...
        """

        def addPointToExtent(point):
            x = point.getX()
            y = point.getY()
            if self.mx is None: # first iteration, set to first point
                self.mx, self.my = x,y
                self.Mx, self.My = x+1,y+1 # HACK. TODO: Find better way ASAP!
            self.mx = min(self.mx,x)
            self.my = min(self.my,y)
            self.Mx = max(self.Mx,x)
            self.My = max(self.My,y)

        crit_geoms = 5
        try:
            if g == None: return
            n = g.__class__.__name__
            if n == "OGPoint": addPointToExtent(g)

            # hack: if the layer consists of a lot of geometries, then add only
            # the first point of geometry to the extent. If it consists of only
            # a few shapes, add EVERY SINGLE POINT to the extent. 
            elif n == "OGLineString":
                if self.layer.totalsize <= crit_geoms:
                    for i in range(len(g._points)):
                        addPointToExtent(g._points[i])
                else:
                    addPointToExtent(g._points[0])
            elif n == "OGPolygon":
                if self.layer.totalsize <= crit_geoms:
                    for i in range(len(g._lines[0]._points)):
                        addPointToExtent(g._lines[0]._points[i])
                else:
                    addPointToExtent(g._lines[0]._points[0])

            # in case of a collection, repeat process for all subgeometries
            elif n == "OGGeometryCollection":
                for subg in g.getGeometries():
                    self.__addGeometryToExtents(subg)
            elif n[:7] == "OGMulti":
                for subg in g.getGeometries():
                    self.__addGeometryToExtents(subg)
        except Exception, e:
            print e

#-------------------------------------------------------------------------------
# Packed in a function to be seen by the class browser
def registerGObjects():
    gobject.type_register(LayerStyle)
    gobject.type_register(Layer)
    gobject.type_register(ResultSet)
registerGObjects()
